package io.noties.markwon.base.image

import android.graphics.*
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import androidx.annotation.IntRange
import io.noties.markwon.base.image.load.AsyncDrawableLoader
import io.noties.markwon.base.image.size.ImageSizeResolver

open class AsyncDrawable(
    val destination: String,
    private val loader: AsyncDrawableLoader,
    /**
     * @since 4.0.0
     */
    val imageSizeResolver: ImageSizeResolver,
    /**
     * @since 4.0.0
     */
    val imageSize: ImageSize?,

    val isBlock: Boolean = true,
    val isSelfLoad:Boolean = false
) : Drawable() {
    // @since 4.5.0
    var placeholder: Drawable? = null

    var resultDrawable: Drawable? = null

    private var callbackSave: Callback? = null

    /**
     * @see .hasKnownDimensions
     * @since 4.0.0
     */
    var lastKnownCanvasWidth = 0
        private set

    /**
     * @see .hasKnownDimensions
     * @since 4.0.0
     */
    var lastKnowTextSize = 0f
        private set

    // @since 2.0.1 for use-cases when image is loaded faster than span is drawn and knows canvas width
    private var waitingForDimensions = false

    // @since 4.5.0 in case if result is Animatable and this drawable was detached, we
    //  keep the state to resume when we are going to be attached again (when used in RecyclerView)
    private var wasPlayingBefore = false

    /**
     * @since 1.0.1
     */
    init {
        placeholder = loader.placeholder()
        placeholder?.let { setPlaceholderResult(it) }
    }

    /**
     * @since 4.2.1
     */
    fun hasKnownDimensions(): Boolean {
        return lastKnownCanvasWidth > 0
    }

    fun hasResult(): Boolean {
        return resultDrawable != null
    }

    val isAttached: Boolean
        get() = callbackSave != null

    fun setCallback2(cb: Callback?) {

        // @since 4.2.1
        //  wrap callback so invalidation happens to this AsyncDrawable instance
        //  and not for wrapped result/placeholder
        callbackSave = if (cb == null) null else WrappedCallback(cb)
        super.setCallback(cb)

        // if not null -> means we are attached
        if (callbackSave != null) {

            // as we have a placeholder now, it's important to check it our placeholder
            // has a proper callback at this point. This is not required in most cases,
            // as placeholder should be static, but if it's not -> it can operate as usual
            if (resultDrawable != null
                && resultDrawable!!.callback == null
            ) {
                resultDrawable!!.callback = callbackSave
            }

            // @since 4.5.0 we trigger loading only if we have no result (and result is not placeholder)
            val shouldLoad = resultDrawable == null || resultDrawable === placeholder
            if (resultDrawable != null) {
                resultDrawable!!.callback = callbackSave

                // @since 4.5.0
                if (resultDrawable is Animatable && wasPlayingBefore) {
                    (resultDrawable as Animatable).start()
                }
            }
            if (shouldLoad) {
                loader.load(this)
            }
        } else {
            if (resultDrawable != null) {
                resultDrawable!!.callback = null

                // let's additionally stop if it Animatable
                if (resultDrawable is Animatable) {
                    val animatable = resultDrawable as Animatable
                    wasPlayingBefore = animatable.isRunning
                    if (wasPlayingBefore) {
                        animatable.stop()
                    }
                }
            }
            loader.cancel(this)
        }
    }

    /**
     * @since 3.0.1
     */
    protected fun setPlaceholderResult(placeholder: Drawable) {
        // okay, if placeholder has bounds -> use it, otherwise use original imageSize
        // it's important to NOT pass to imageSizeResolver when placeholder has bounds
        // this is done, so actual result and placeholder can have _different_
        // bounds. Assume image is loaded with HTML and has ImageSize width=100%,
        // so, even if placeholder has exact bounds, it will still be scaled up.

        // this condition should not be true for placeholder (at least for now)
        // (right now this method is always called from constructor)

        // but it is, unregister current result
        resultDrawable?.callback = null

        val rect = placeholder.bounds
        if (rect.isEmpty) {
            // check for intrinsic bounds
            val intrinsic = DrawableUtils.intrinsicBounds(placeholder)
            if (intrinsic.isEmpty) {
                // @since 4.2.2
                // if intrinsic bounds are empty, use _any_ non-empty bounds,
                // they must be non-empty so when result is obtained - proper invalidation will occur
                // (0, 0, 1, 0) is still considered empty
                placeholder.setBounds(0, 0, 1, 1)
            } else {
                // use them
                placeholder.bounds = intrinsic
            }

            // it is very important (if we have a placeholder) to set own bounds to it (and they must not be empty
            // otherwise result won't be rendered)
            // @since 4.2.2
            bounds = placeholder.bounds
            setResult(placeholder)
        } else {

            // this method is not the same as above, as we do not want to trigger image-size-resolver
            // in case when placeholder has exact bounds

            // placeholder has bounds specified -> use them until we have real result
            resultDrawable = placeholder
            resultDrawable?.callback = callbackSave

            // use bounds directly
            bounds = rect

            // just in case -> so we do not update placeholder when we have canvas dimensions
            waitingForDimensions = false
        }
    }

    open fun setResult(result: Drawable) {

        // @since 4.5.0 revert this flag when we have new source
        wasPlayingBefore = false

        // if we have previous one, detach it
        resultDrawable?.callback = null

        this.resultDrawable = result

        callbackSave?.let {
            this.resultDrawable?.callback=it
        }

        initBounds()
    }

    /**
     * Remove result from this drawable (for example, in case of cancellation)
     *
     * @since 3.0.1
     */
    fun clearResult() {
        resultDrawable?.let {
            it.callback = null
            // clear bounds
            setBounds(0, 0, 0, 0)
        }
        resultDrawable = null
    }

    private fun initBounds() {
        if (lastKnownCanvasWidth == 0) {
            // we still have no bounds - wait for them
            waitingForDimensions = true

            // we cannot have empty bounds - otherwise in case if text contains
            //  a single AsyncDrawableSpan, it won't be displayed
            bounds = noDimensionsBounds(resultDrawable)
            return
        }
        waitingForDimensions = false
        val bounds = resolveBounds()
        resultDrawable?.bounds = bounds
        // @since 4.2.1, we set callback after bounds are resolved
        //  to reduce number of invalidations
        resultDrawable?.callback = callbackSave

        // so, this method will check if there is previous bounds and call invalidate _BEFORE_
        //  applying new bounds. This is why it is important to have initial bounds empty.
        setBounds(bounds)
        invalidateSelf()
    }

    /**
     * @since 1.0.1
     */
    fun initWithKnownDimensions(width: Int, textSize: Float) {
        lastKnownCanvasWidth = width
        lastKnowTextSize = textSize
        if (waitingForDimensions) {
            initBounds()
        }
    }

    override fun draw(canvas: Canvas) {
        resultDrawable?.draw(canvas)
    }

    override fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) {}

    override fun setColorFilter(colorFilter: ColorFilter?) {}

    @Deprecated("Deprecated in Java", ReplaceWith(
        "resultDrawable?.opacity ?: PixelFormat.TRANSPARENT",
        "android.graphics.PixelFormat"
    )
    )
    override fun getOpacity(): Int {
        return resultDrawable?.opacity?:PixelFormat.TRANSPARENT
    }

    override fun getIntrinsicWidth(): Int {

        return resultDrawable?.intrinsicWidth?:1
    }

    override fun getIntrinsicHeight(): Int {

        return resultDrawable?.intrinsicHeight?:1
    }

    /**
     * @since 1.0.1
     */
    private fun resolveBounds(): Rect {
        // @since 2.0.0 previously we were checking if image is greater than canvas width here
        //          but as imageSizeResolver won't be null anymore, we should transfer this logic
        //          there
        return imageSizeResolver.resolveImageSize(this)
    }

    override fun toString(): String {
        return "AsyncDrawable{" +
                "destination='" + destination + '\'' +
                ", imageSize=" + imageSize +
                ", result=" + resultDrawable +
                ", canvasWidth=" + lastKnownCanvasWidth +
                ", textSize=" + lastKnowTextSize +
                ", waitingForDimensions=" + waitingForDimensions +
                '}'
    }

    // @since 4.2.1
    //  Wrapped callback to trigger invalidation for this AsyncDrawable instance (and not result/placeholder)
    private inner class WrappedCallback internal constructor(private val callback: Callback) :
        Callback {
        override fun invalidateDrawable(who: Drawable) {
            callback.invalidateDrawable(this@AsyncDrawable)
        }

        override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
            callback.scheduleDrawable(this@AsyncDrawable, what, `when`)
        }

        override fun unscheduleDrawable(who: Drawable, what: Runnable) {
            callback.unscheduleDrawable(this@AsyncDrawable, what)
        }
    }

    companion object {
        /**
         * @since 4.3.0
         */
        private fun noDimensionsBounds(result: Drawable?): Rect {
            if (result != null) {
                val bounds = result.bounds
                if (!bounds.isEmpty) {
                    return bounds
                }
                val intrinsicBounds = DrawableUtils.intrinsicBounds(result)
                if (!intrinsicBounds.isEmpty) {
                    return intrinsicBounds
                }
            }
            return Rect(0, 0, 1, 1)
        }
    }
}